/* Copyright (c) 2006, 2009, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */
 
package com.cburch.logisim.tools;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;

import com.cburch.logisim.circuit.Circuit;
import com.cburch.logisim.circuit.CircuitEvent;
import com.cburch.logisim.circuit.CircuitListener;
import com.cburch.logisim.circuit.Wire;
import com.cburch.logisim.comp.Component;
import com.cburch.logisim.comp.ComponentDrawContext;
import com.cburch.logisim.comp.ComponentFactory;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeSet;
import com.cburch.logisim.data.Direction;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.data.Value;
import com.cburch.logisim.gui.main.AttributeTableListener;
import com.cburch.logisim.gui.main.Canvas;
import com.cburch.logisim.gui.main.Selection;
import com.cburch.logisim.gui.main.Selection.Event;
import com.cburch.logisim.proj.Project;
import com.cburch.logisim.UItools.GraphicsUtil;
import com.cburch.logisim.util.Strings;

public class EditTool extends Tool {
    private static final int CACHE_MAX_SIZE = 32;
    private static final Location NULL_LOCATION
        = Location.create(Integer.MIN_VALUE, Integer.MIN_VALUE);
    
    private class Listener implements CircuitListener, Selection.Listener {
        public void circuitChanged(CircuitEvent event) {
            lastX = -1;
            cache.clear();
            updateLocation(lastCanvas, lastRawX, lastRawY, lastMods);
        }

        public void selectionChanged(Event event) {
            lastX = -1;
            cache.clear();
            updateLocation(lastCanvas, lastRawX, lastRawY, lastMods);
        }
    }
    
    private Listener listener;
    private SelectTool select;
    private WiringTool wiring;
    private Tool current;
    private LinkedHashMap cache;
    private Canvas lastCanvas;
    private int lastRawX;
    private int lastRawY;
    private int lastX; // last coordinates where wiring was computed
    private int lastY;
    private int lastMods; // last modifiers for mouse event
    private Location wireLoc; // coordinates where to draw wiring indicator, if
    private int pressX; // last coordinate where mouse was pressed
    private int pressY; // (used to determine when a short wire has been clicked)
    
    public EditTool(SelectTool select, WiringTool wiring) {
        this.listener = new Listener();
        this.select = select;
        this.wiring = wiring;
        this.current = select;
        this.cache = new LinkedHashMap();
        this.lastX = -1;
        this.wireLoc = NULL_LOCATION;
        this.pressX = -1;
    }
    
    public String getName() {
        return "Edit Tool";
    }
    
    public String getDisplayName() {
        return Strings.get("editTool", Strings.toolsSource);
    }
    
    public String getDescription() {
        return Strings.get("editToolDesc", Strings.toolsSource);
    }

    public AttributeSet getAttributeSet() {
        return select.getAttributeSet();
    }
    
    public AttributeTableListener getAttributeTableListener(Project proj) {
        return select.getAttributeTableListener(proj);
    }
    
    public void setAttributeSet(AttributeSet attrs) {
        select.setAttributeSet(attrs);
    }
    
    public void paintIcon(ComponentDrawContext c, int x, int y) {
        select.paintIcon(c, x, y);
    }

    public void draw(Canvas canvas, ComponentDrawContext context) {
        Location loc = wireLoc;
        if(loc != NULL_LOCATION) {
            int x = loc.getX();
            int y = loc.getY();
            Graphics g = context.getGraphics();
            g.setColor(Value.TRUE_COLOR);
            GraphicsUtil.switchToWidth(g, 2);
            g.drawOval(x - 5, y - 5, 10, 10);
            g.setColor(Color.BLACK);
            GraphicsUtil.switchToWidth(g, 1);
        }
        current.draw(canvas, context);
    }
    
    public void select(Canvas canvas) {
        current = select;
        lastCanvas = canvas;
        cache.clear();
        canvas.getCircuit().addCircuitListener(listener);
        canvas.getSelection().addListener(listener);
    }
    
    public void deselect(Canvas canvas) {
        current = select;
        canvas.getSelection().setSuppressHandles(null);
        cache.clear();
        canvas.getCircuit().removeCircuitListener(listener);
        canvas.getSelection().removeListener(listener);
    }

    public void mousePressed(Canvas canvas, Graphics g, MouseEvent e) {
        boolean wire = updateLocation(canvas, e);
        Location oldWireLoc = wireLoc;
        wireLoc = NULL_LOCATION;
        lastX = Integer.MIN_VALUE;
        if(wire) {
            current = wiring;
            Selection sel = canvas.getSelection();
            Circuit circ = canvas.getCircuit();
            Collection selected = sel.getAnchoredComponents();
            ArrayList suppress = null;
            for(Iterator it = circ.getWires().iterator(); it.hasNext(); ) {
                Object w = it.next();
                if(selected.contains(w)) {
                    Wire w2 = (Wire) w;
                    if(w2.contains(oldWireLoc)) {
                        if(suppress == null) suppress = new ArrayList();
                        suppress.add(w);
                    }
                }
            }
            sel.setSuppressHandles(suppress);
        } else {
            current = select;
        }
        pressX = e.getX();
        pressY = e.getY();
        current.mousePressed(canvas, g, e);
    }
    
    public void mouseDragged(Canvas canvas, Graphics g, MouseEvent e) {
        isClick(e);
        current.mouseDragged(canvas, g, e);
    }
    
    public void mouseReleased(Canvas canvas, Graphics g, MouseEvent e) {
        boolean click = isClick(e) && current == wiring;
        canvas.getSelection().setSuppressHandles(null);
        current.mouseReleased(canvas, g, e);
        if(click) {
            select.mousePressed(canvas, g, e);
            select.mouseReleased(canvas, g, e);
        }
        current = select;
        cache.clear();
        updateLocation(canvas, e);
    }
    
    public void mouseEntered(Canvas canvas, Graphics g, MouseEvent e) {
        pressX = -1;
        current.mouseEntered(canvas, g, e);
        canvas.requestFocusInWindow();
    }
    
    public void mouseExited(Canvas canvas, Graphics g, MouseEvent e) {
        pressX = -1;
        current.mouseExited(canvas, g, e);
    }
    
    public void mouseMoved(Canvas canvas, Graphics g, MouseEvent e) {
        updateLocation(canvas, e);
        select.mouseMoved(canvas, g, e);
    }
    
    private boolean isClick(MouseEvent e) {
        int px = pressX;
        if(px < 0) {
            return false;
        } else {
            int dx = e.getX() - px;
            int dy = e.getY() - pressY;
            if(dx * dx + dy * dy <= 4) {
                return true;
            } else {
                pressX = -1;
                return false;
            }
        }
    }
    
    private boolean updateLocation(Canvas canvas, MouseEvent e) {
        return updateLocation(canvas, e.getX(), e.getY(), e.getModifiersEx());
    }
    
    private boolean updateLocation(Canvas canvas, KeyEvent e) {
        int x = lastRawX;
        if(x >= 0) return updateLocation(canvas, x, lastRawY, e.getModifiersEx());
        else return false;
    }
    
    private boolean updateLocation(Canvas canvas, int mx, int my, int mods) {
        int snapx = Canvas.snapXToGrid(mx);
        int snapy = Canvas.snapYToGrid(my);
        int dx = mx - snapx;
        int dy = my - snapy;
        boolean isEligible = dx * dx + dy * dy < 36;
        if((mods & MouseEvent.ALT_DOWN_MASK) != 0) isEligible = true;
        if(!isEligible) {
            snapx = -1;
            snapy = -1;
        }
        boolean modsSame = lastMods == mods;
        lastCanvas = canvas;
        lastRawX = mx;
        lastRawY = my;
        lastMods = mods;
        if(lastX == snapx && lastY == snapy && modsSame) { // already computed
            return wireLoc != NULL_LOCATION;
        } else {
            Location snap = Location.create(snapx, snapy);
            if(modsSame) {
                Object o = cache.get(snap);
                if(o != null) {
                    lastX = snapx;
                    lastY = snapy;
                    canvas.repaint();
                    boolean ret = ((Boolean) o).booleanValue();
                    wireLoc = ret ? snap : NULL_LOCATION;
                    return ret;
                }
            } else {
                cache.clear();
            }

            boolean ret = isEligible && isWiringPoint(canvas, snap, mods);
            wireLoc = ret ? snap : NULL_LOCATION;
            cache.put(snap, Boolean.valueOf(ret));
            int toRemove = cache.size() - CACHE_MAX_SIZE;
            for(Iterator it = cache.keySet().iterator(); it.hasNext() && toRemove > 0; ) {
                it.next();
                it.remove();
                toRemove--;
            }

            lastX = snapx;
            lastY = snapy;
            canvas.repaint();
            return ret;
        }
    }
    
    private boolean isWiringPoint(Canvas canvas, Location loc, int modsEx) {
        boolean wiring = (modsEx & MouseEvent.ALT_DOWN_MASK) == 0;
        boolean select = !wiring;
        
        if(canvas != null && canvas.getSelection() != null) {
            Collection sel = canvas.getSelection().getComponents();
            if(sel != null) {
                for(Iterator it = sel.iterator(); it.hasNext(); ) {
                    Component c = (Component) it.next();
                    if(c instanceof Wire && c.contains(loc)) return select;
                }
            }
        }
        
        Circuit circ = canvas.getCircuit();
        Collection at = circ.getComponents(loc);
        if(at != null && at.size() > 0) return wiring;
        
        for(Iterator it = circ.getWires().iterator(); it.hasNext(); ) {
            Wire w = (Wire) it.next();
            if(w.contains(loc)) { return wiring; }
        }
        return select;
    }

    public void keyTyped(Canvas canvas, KeyEvent e) {
        select.keyTyped(canvas, e);
    }
    
    public void keyPressed(Canvas canvas, KeyEvent e) {
        switch(e.getKeyCode()) {
        case KeyEvent.VK_BACK_SPACE:
        case KeyEvent.VK_DELETE:
            canvas.getProject().doAction(canvas.getSelection().deleteAll());
            e.consume();
            break;
        case KeyEvent.VK_INSERT:
            canvas.getProject().doAction(canvas.getSelection().duplicate());
            e.consume();
            break;
        case KeyEvent.VK_UP:    attemptReface(canvas, Direction.NORTH, e); break;
        case KeyEvent.VK_DOWN:  attemptReface(canvas, Direction.SOUTH, e); break;
        case KeyEvent.VK_LEFT:  attemptReface(canvas, Direction.WEST, e); break;
        case KeyEvent.VK_RIGHT: attemptReface(canvas, Direction.EAST, e); break;
        case KeyEvent.VK_ALT:   updateLocation(canvas, e); e.consume(); break;
        default:
            select.keyPressed(canvas, e);
        }
    }
    
    public void keyReleased(Canvas canvas, KeyEvent e) {
        switch(e.getKeyCode()) {
        case KeyEvent.VK_ALT:   updateLocation(canvas, e); e.consume(); break;
        default:
            select.keyReleased(canvas, e);
        }
    }
    
    private void attemptReface(Canvas canvas, Direction facing, KeyEvent e) {
        Selection sel = canvas.getSelection();
        HashMap oldFacings = new HashMap();
        Object key = ComponentFactory.FACING_ATTRIBUTE_KEY;
        for(Iterator it = sel.getAnchoredComponents().iterator(); it.hasNext(); ) {
            Component comp = (Component) it.next();
            if(!(comp instanceof Wire)) {
                Attribute attr = (Attribute) comp.getFactory().getFeature(key, comp.getAttributeSet());
                if(attr == null || comp.getAttributeSet().isReadOnly(attr)) return;
                Object oldValue = comp.getAttributeValue(attr);
                if(!oldValue.equals(facing)) oldFacings.put(comp, oldValue);
            }
        }
        for(Iterator it = sel.getFloatingComponents().iterator(); it.hasNext(); ) {
            Component comp = (Component) it.next();
            if(!(comp instanceof Wire)) {
                Attribute attr = (Attribute) comp.getFactory().getFeature(key, comp.getAttributeSet());
                if(attr == null) return;
                Object oldValue = comp.getAttributeValue(attr);
                if(!oldValue.equals(facing)) oldFacings.put(comp, oldValue);
            }
        }
        if(!oldFacings.isEmpty()) {
            canvas.getProject().doAction(new SelectionReface(canvas.getCircuit(),
                    oldFacings, facing));
            e.consume();
        }
    }
    
    public Cursor getCursor() {
        return select.getCursor(); 
    }
}
